Skip to main content

salvo_oapi/openapi/
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::collections::BTreeMap;
7use std::iter;
8
9use serde::{Deserialize, Deserializer, Serialize};
10
11use crate::PropMap;
12
13/// OpenAPI [security requirement][security] object.
14///
15/// Security requirement holds list of required [`SecurityScheme`] *names* and possible *scopes*
16/// required to execute the operation. They can be defined in
17/// [`#[salvo_oapi::endpoint(...)]`][endpoint].
18///
19/// Applying the security requirement to [`OpenApi`][openapi] will make it globally
20/// available to all operations. When applied to specific [`#[salvo_oapi::endpoint(...)]`][endpoint]
21/// will only make the security requirements available for that operation. Only one of the
22/// requirements must be satisfied.
23///
24/// [security]: https://spec.openapis.org/oas/latest.html#security-requirement-object
25/// [endpoint]: ../../attr.endpoint.html
26/// [openapi]: ../../derive.OpenApi.html
27#[derive(Serialize, Deserialize, Debug, Ord, PartialOrd, Default, Clone, PartialEq, Eq)]
28pub struct SecurityRequirement {
29    #[serde(flatten)]
30    pub(crate) value: BTreeMap<String, Vec<String>>,
31}
32
33impl SecurityRequirement {
34    /// Construct a new [`SecurityRequirement`]
35    ///
36    /// Accepts name for the security requirement which must match to the name of available
37    /// [`SecurityScheme`]. Second parameter is [`IntoIterator`] of [`Into<String>`] scopes
38    /// needed by the [`SecurityRequirement`]. Scopes must match to the ones defined in
39    /// [`SecurityScheme`].
40    ///
41    /// As of OpenAPI 3.2 the name may instead be the URI of a Security Scheme Object. A name
42    /// identical to a component name is always resolved as a component name, so to reference a
43    /// scheme by a single-segment relative URI that collides with a component name, prefix it
44    /// with `./` (e.g. `./foo`). No such resolution is performed here — the name is stored
45    /// verbatim.
46    ///
47    /// # Examples
48    ///
49    /// Creates a new security requirement with scopes.
50    /// ```
51    /// # use salvo_oapi::security::SecurityRequirement;
52    /// SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
53    /// ```
54    ///
55    /// Reference a security scheme by URI (OpenAPI 3.2).
56    /// ```
57    /// # use salvo_oapi::security::SecurityRequirement;
58    /// SecurityRequirement::new("https://example.com/schemes.json#/oauth", ["read:items"]);
59    /// ```
60    ///
61    /// You can also create an empty security requirement with `Default::default()`.
62    /// ```
63    /// # use salvo_oapi::security::SecurityRequirement;
64    /// SecurityRequirement::default();
65    /// ```
66    #[must_use]
67    pub fn new<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
68        name: N,
69        scopes: S,
70    ) -> Self {
71        Self {
72            value: BTreeMap::from_iter(iter::once_with(|| {
73                (
74                    Into::<String>::into(name),
75                    scopes
76                        .into_iter()
77                        .map(|scope| Into::<String>::into(scope))
78                        .collect::<Vec<_>>(),
79                )
80            })),
81        }
82    }
83
84    /// Check if the security requirement is empty.
85    #[must_use]
86    pub fn is_empty(&self) -> bool {
87        self.value.is_empty()
88    }
89
90    /// Allows to add multiple names to security requirement.
91    ///
92    /// Accepts name for the security requirement which must match to the name of available
93    /// [`SecurityScheme`]. Second parameter is [`IntoIterator`] of [`Into<String>`] scopes
94    /// needed by the [`SecurityRequirement`]. Scopes must match to the ones defined in
95    /// [`SecurityScheme`].
96    #[must_use]
97    pub fn add<N: Into<String>, S: IntoIterator<Item = I>, I: Into<String>>(
98        mut self,
99        name: N,
100        scopes: S,
101    ) -> Self {
102        self.value.insert(
103            Into::<String>::into(name),
104            scopes.into_iter().map(Into::<String>::into).collect(),
105        );
106
107        self
108    }
109}
110
111/// OpenAPI [security scheme][security] for path operations.
112///
113/// [security]: https://spec.openapis.org/oas/latest.html#security-scheme-object
114///
115/// # Examples
116///
117/// Create implicit OAuth2 flow security schema for path operations.
118/// ```
119/// # use salvo_oapi::security::{SecurityScheme, OAuth2, Implicit, Flow, Scopes};
120/// SecurityScheme::OAuth2(OAuth2::with_description(
121///     [Flow::Implicit(Implicit::new(
122///         "https://localhost/auth/dialog",
123///         Scopes::from_iter([
124///             ("edit:items", "edit my items"),
125///             ("read:items", "read my items"),
126///         ]),
127///     ))],
128///     "my oauth2 flow",
129/// ));
130/// ```
131///
132/// Create JWT header authentication.
133/// ```
134/// # use salvo_oapi::security::{SecurityScheme, HttpAuthScheme, Http};
135/// SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT"));
136/// ```
137#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
138#[serde(tag = "type", rename_all = "camelCase")]
139pub enum SecurityScheme {
140    /// OAuth flow authentication.
141    #[serde(rename = "oauth2")]
142    OAuth2(OAuth2),
143    /// Api key authentication sent in *`header`*, *`cookie`* or *`query`*.
144    ApiKey(ApiKey),
145    /// Http authentication such as *`bearer`* or *`basic`*.
146    Http(Http),
147    /// OpenID Connect URL to discover OAuth2 configuration values.
148    OpenIdConnect(OpenIdConnect),
149    /// Authentication is done via client side certificate.
150    ///
151    /// OpenApi 3.1 type
152    #[serde(rename = "mutualTLS")]
153    MutualTls {
154        /// Description information.
155        #[serde(skip_serializing_if = "Option::is_none")]
156        description: Option<String>,
157        /// Declares this security scheme deprecated. Added in OpenAPI 3.2.
158        #[serde(skip_serializing_if = "Option::is_none", default)]
159        deprecated: Option<bool>,
160    },
161}
162impl From<OAuth2> for SecurityScheme {
163    fn from(oauth2: OAuth2) -> Self {
164        Self::OAuth2(oauth2)
165    }
166}
167impl From<ApiKey> for SecurityScheme {
168    fn from(api_key: ApiKey) -> Self {
169        Self::ApiKey(api_key)
170    }
171}
172impl From<OpenIdConnect> for SecurityScheme {
173    fn from(open_id_connect: OpenIdConnect) -> Self {
174        Self::OpenIdConnect(open_id_connect)
175    }
176}
177
178/// Api key authentication [`SecurityScheme`].
179#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
180#[serde(tag = "in", rename_all = "lowercase")]
181pub enum ApiKey {
182    /// Create api key which is placed in HTTP header.
183    Header(ApiKeyValue),
184    /// Create api key which is placed in query parameters.
185    Query(ApiKeyValue),
186    /// Create api key which is placed in cookie value.
187    Cookie(ApiKeyValue),
188}
189
190/// Value object for [`ApiKey`].
191#[non_exhaustive]
192#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
193pub struct ApiKeyValue {
194    /// Name of the [`ApiKey`] parameter.
195    pub name: String,
196
197    /// Description of the [`ApiKey`] [`SecurityScheme`]. Supports markdown syntax.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub description: Option<String>,
200
201    /// Declares this security scheme deprecated. Added in OpenAPI 3.2; defaults to `false`.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub deprecated: Option<bool>,
204
205    /// Optional extensions "x-something"
206    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
207    pub extensions: PropMap<String, serde_json::Value>,
208}
209
210impl ApiKeyValue {
211    /// Constructs new api key value.
212    ///
213    /// # Examples
214    ///
215    /// Creates a new API key security schema named `api_key`.
216    /// ```
217    /// # use salvo_oapi::security::ApiKeyValue;
218    /// let api_key = ApiKeyValue::new("api_key");
219    /// ```
220    pub fn new<S: Into<String>>(name: S) -> Self {
221        Self {
222            name: name.into(),
223            description: None,
224            deprecated: None,
225            extensions: Default::default(),
226        }
227    }
228
229    /// Construct a new api key with optional description supporting markdown syntax.
230    ///
231    /// # Examples
232    ///
233    /// Creates a new API key security schema named `api_key` with a description.
234    /// ```
235    /// # use salvo_oapi::security::ApiKeyValue;
236    /// let api_key = ApiKeyValue::with_description("api_key", "my api_key token");
237    /// ```
238    pub fn with_description<S: Into<String>>(name: S, description: S) -> Self {
239        Self {
240            name: name.into(),
241            description: Some(description.into()),
242            deprecated: None,
243            extensions: Default::default(),
244        }
245    }
246
247    /// Mark this [`ApiKeyValue`] deprecated. Requires OpenAPI 3.2.
248    #[must_use]
249    pub fn deprecated(mut self, deprecated: bool) -> Self {
250        self.deprecated = Some(deprecated);
251        self
252    }
253
254    /// Add openapi extensions (`x-something`) for [`ApiKeyValue`].
255    #[must_use]
256    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
257        self.extensions = extensions;
258        self
259    }
260}
261
262/// Http authentication [`SecurityScheme`] builder.
263///
264/// Methods can be chained to configure _bearer_format_ or to add _description_.
265#[non_exhaustive]
266#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
267#[serde(rename_all = "camelCase")]
268pub struct Http {
269    /// Http authorization scheme in HTTP `Authorization` header value.
270    pub scheme: HttpAuthScheme,
271
272    /// Optional hint to client how the bearer token is formatted. Valid only with
273    /// [`HttpAuthScheme::Bearer`].
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub bearer_format: Option<String>,
276
277    /// Optional description of [`Http`] [`SecurityScheme`] supporting markdown syntax.
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub description: Option<String>,
280
281    /// Declares this security scheme deprecated. Added in OpenAPI 3.2; defaults to `false`.
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub deprecated: Option<bool>,
284
285    /// Optional extensions "x-something"
286    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
287    pub extensions: PropMap<String, serde_json::Value>,
288}
289
290impl Http {
291    /// Creates a new HTTP authentication security schema.
292    ///
293    /// Accepts one argument which defines the scheme of the http authentication.
294    ///
295    /// # Examples
296    ///
297    /// Create http security schema with basic authentication.
298    /// ```
299    /// # use salvo_oapi::security::{SecurityScheme, Http, HttpAuthScheme};
300    /// SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
301    /// ```
302    #[must_use]
303    pub fn new(scheme: HttpAuthScheme) -> Self {
304        Self {
305            scheme,
306            bearer_format: None,
307            description: None,
308            deprecated: None,
309            extensions: Default::default(),
310        }
311    }
312    /// Add or change http authentication scheme used.
313    #[must_use]
314    pub fn scheme(mut self, scheme: HttpAuthScheme) -> Self {
315        self.scheme = scheme;
316
317        self
318    }
319    /// Add or change informative bearer format for http security schema.
320    ///
321    /// This is only applicable to [`HttpAuthScheme::Bearer`].
322    ///
323    /// # Examples
324    ///
325    /// Add JTW bearer format for security schema.
326    /// ```
327    /// # use salvo_oapi::security::{Http, HttpAuthScheme};
328    /// Http::new(HttpAuthScheme::Bearer).bearer_format("JWT");
329    /// ```
330    #[must_use]
331    pub fn bearer_format<S: Into<String>>(mut self, bearer_format: S) -> Self {
332        if self.scheme == HttpAuthScheme::Bearer {
333            self.bearer_format = Some(bearer_format.into());
334        }
335
336        self
337    }
338
339    /// Add or change optional description supporting markdown syntax.
340    #[must_use]
341    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
342        self.description = Some(description.into());
343
344        self
345    }
346
347    /// Mark this [`Http`] security scheme deprecated. Requires OpenAPI 3.2.
348    #[must_use]
349    pub fn deprecated(mut self, deprecated: bool) -> Self {
350        self.deprecated = Some(deprecated);
351        self
352    }
353
354    /// Add openapi extensions (`x-something`) for [`Http`].
355    #[must_use]
356    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
357        self.extensions = extensions;
358        self
359    }
360}
361
362/// Implements types according [RFC7235](https://datatracker.ietf.org/doc/html/rfc7235#section-5.1).
363///
364/// Types are maintained at <https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml>.
365#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
366#[serde(rename_all = "lowercase")]
367pub enum HttpAuthScheme {
368    /// Basic authentication scheme.
369    #[default]
370    Basic,
371    /// Bearer authentication scheme.
372    Bearer,
373    /// Digest authentication scheme.
374    Digest,
375    /// HOBA authentication scheme.
376    Hoba,
377    /// Mutual authentication scheme.
378    Mutual,
379    /// Negotiate authentication scheme.
380    Negotiate,
381    /// OAuth authentication scheme.
382    OAuth,
383    /// ScramSha1 authentication scheme.
384    #[serde(rename = "scram-sha-1")]
385    ScramSha1,
386    /// ScramSha256 authentication scheme.
387    #[serde(rename = "scram-sha-256")]
388    ScramSha256,
389    /// Vapid authentication scheme.
390    Vapid,
391}
392
393/// OpenID Connect [`SecurityScheme`].
394#[non_exhaustive]
395#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
396#[serde(rename_all = "camelCase")]
397pub struct OpenIdConnect {
398    /// Url of the [`OpenIdConnect`] to discover OAuth2 connect values.
399    pub open_id_connect_url: String,
400
401    /// Description of [`OpenIdConnect`] [`SecurityScheme`] supporting markdown syntax.
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub description: Option<String>,
404
405    /// Declares this security scheme deprecated. Added in OpenAPI 3.2; defaults to `false`.
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub deprecated: Option<bool>,
408
409    /// Optional extensions "x-something"
410    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
411    pub extensions: PropMap<String, serde_json::Value>,
412}
413
414impl OpenIdConnect {
415    /// Construct a new open id connect security schema.
416    ///
417    /// # Examples
418    ///
419    /// ```
420    /// # use salvo_oapi::security::OpenIdConnect;
421    /// OpenIdConnect::new("https://localhost/openid");
422    /// ```
423    pub fn new<S: Into<String>>(open_id_connect_url: S) -> Self {
424        Self {
425            open_id_connect_url: open_id_connect_url.into(),
426            description: None,
427            deprecated: None,
428            extensions: Default::default(),
429        }
430    }
431
432    /// Construct a new [`OpenIdConnect`] [`SecurityScheme`] with optional description
433    /// supporting markdown syntax.
434    ///
435    /// # Examples
436    ///
437    /// ```
438    /// # use salvo_oapi::security::OpenIdConnect;
439    /// OpenIdConnect::with_description("https://localhost/openid", "my pet api open id connect");
440    /// ```
441    pub fn with_description<S: Into<String>>(open_id_connect_url: S, description: S) -> Self {
442        Self {
443            open_id_connect_url: open_id_connect_url.into(),
444            description: Some(description.into()),
445            deprecated: None,
446            extensions: Default::default(),
447        }
448    }
449
450    /// Mark this [`OpenIdConnect`] security scheme deprecated. Requires OpenAPI 3.2.
451    #[must_use]
452    pub fn deprecated(mut self, deprecated: bool) -> Self {
453        self.deprecated = Some(deprecated);
454        self
455    }
456
457    /// Add openapi extensions (`x-something`) for [`OpenIdConnect`].
458    #[must_use]
459    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
460        self.extensions = extensions;
461        self
462    }
463}
464
465/// OAuth2 [`Flow`] configuration for [`SecurityScheme`].
466#[non_exhaustive]
467#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
468#[serde(rename_all = "camelCase")]
469pub struct OAuth2 {
470    /// Map of supported OAuth2 flows, keyed by the flow name defined by the
471    /// [OAuth Flows Object][flows].
472    ///
473    /// [flows]: https://spec.openapis.org/oas/v3.2.0.html#oauth-flows-object
474    #[serde(deserialize_with = "deserialize_flows")]
475    pub flows: PropMap<String, Flow>,
476
477    /// Optional description for the [`OAuth2`] [`Flow`] [`SecurityScheme`].
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub description: Option<String>,
480
481    /// URL to the OAuth2 authorization server metadata
482    /// ([RFC8414](https://datatracker.ietf.org/doc/html/rfc8414)). TLS is required.
483    /// Added in OpenAPI 3.2.
484    #[serde(rename = "oauth2MetadataUrl", skip_serializing_if = "Option::is_none")]
485    pub oauth2_metadata_url: Option<String>,
486
487    /// Declares this security scheme deprecated. Added in OpenAPI 3.2; defaults to `false`.
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub deprecated: Option<bool>,
490
491    /// Optional extensions "x-something"
492    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
493    pub extensions: PropMap<String, serde_json::Value>,
494}
495
496impl OAuth2 {
497    /// Construct a new OAuth2 security schema configuration object.
498    ///
499    /// OAuth flow accepts a slice of [`Flow`] configuration objects and can be optionally provided
500    /// with description.
501    ///
502    /// # Examples
503    ///
504    /// Creates a new OAuth2 flow with multiple authentication flows.
505    /// ```
506    /// # use salvo_oapi::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
507    /// OAuth2::new([
508    ///     Flow::Password(Password::with_refresh_url(
509    ///         "https://localhost/oauth/token",
510    ///         Scopes::from_iter([
511    ///             ("edit:items", "edit my items"),
512    ///             ("read:items", "read my items"),
513    ///         ]),
514    ///         "https://localhost/refresh/token",
515    ///     )),
516    ///     Flow::AuthorizationCode(AuthorizationCode::new(
517    ///         "https://localhost/authorization/token",
518    ///         "https://localhost/token/url",
519    ///         Scopes::from_iter([
520    ///             ("edit:items", "edit my items"),
521    ///             ("read:items", "read my items"),
522    ///         ]),
523    ///     )),
524    /// ]);
525    /// ```
526    pub fn new<I: IntoIterator<Item = Flow>>(flows: I) -> Self {
527        Self {
528            flows: PropMap::from_iter(
529                flows
530                    .into_iter()
531                    .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
532            ),
533            description: None,
534            oauth2_metadata_url: None,
535            deprecated: None,
536            extensions: Default::default(),
537        }
538    }
539
540    /// Construct a new OAuth2 flow with optional description supporting markdown syntax.
541    ///
542    /// # Examples
543    ///
544    /// Creates a new OAuth2 flow with multiple authentication flows and a description.
545    /// ```
546    /// # use salvo_oapi::security::{OAuth2, Flow, Password, AuthorizationCode, Scopes};
547    /// OAuth2::with_description(
548    ///     [
549    ///         Flow::Password(Password::with_refresh_url(
550    ///             "https://localhost/oauth/token",
551    ///             Scopes::from_iter([
552    ///                 ("edit:items", "edit my items"),
553    ///                 ("read:items", "read my items"),
554    ///             ]),
555    ///             "https://localhost/refresh/token",
556    ///         )),
557    ///         Flow::AuthorizationCode(AuthorizationCode::new(
558    ///             "https://localhost/authorization/token",
559    ///             "https://localhost/token/url",
560    ///             Scopes::from_iter([
561    ///                 ("edit:items", "edit my items"),
562    ///                 ("read:items", "read my items"),
563    ///             ]),
564    ///         )),
565    ///     ],
566    ///     "my oauth2 flow",
567    /// );
568    /// ```
569    pub fn with_description<I: IntoIterator<Item = Flow>, S: Into<String>>(
570        flows: I,
571        description: S,
572    ) -> Self {
573        Self {
574            flows: PropMap::from_iter(
575                flows
576                    .into_iter()
577                    .map(|auth_flow| (String::from(auth_flow.get_type_as_str()), auth_flow)),
578            ),
579            description: Some(description.into()),
580            oauth2_metadata_url: None,
581            deprecated: None,
582            extensions: Default::default(),
583        }
584    }
585
586    /// Set the OAuth2 authorization server metadata URL. Requires OpenAPI 3.2.
587    #[must_use]
588    pub fn oauth2_metadata_url<S: Into<String>>(mut self, oauth2_metadata_url: S) -> Self {
589        self.oauth2_metadata_url = Some(oauth2_metadata_url.into());
590        self
591    }
592
593    /// Mark this [`OAuth2`] security scheme deprecated. Requires OpenAPI 3.2.
594    #[must_use]
595    pub fn deprecated(mut self, deprecated: bool) -> Self {
596        self.deprecated = Some(deprecated);
597        self
598    }
599}
600
601/// Deserialize the [`OAuth2::flows`] map by dispatching on the flow name rather than relying on
602/// [`Flow`]'s untagged representation.
603///
604/// Several flows are structurally indistinguishable — `password` and `clientCredentials` have
605/// identical fields, and `deviceAuthorization` is a superset of both — so an untagged match would
606/// resolve them by declaration order and pick the wrong variant. The map key names the flow, so
607/// use it.
608fn deserialize_flows<'de, D>(deserializer: D) -> Result<PropMap<String, Flow>, D::Error>
609where
610    D: Deserializer<'de>,
611{
612    fn flow<T, E>(name: &str, value: serde_json::Value) -> Result<T, E>
613    where
614        T: serde::de::DeserializeOwned,
615        E: serde::de::Error,
616    {
617        serde_json::from_value(value)
618            .map_err(|e| E::custom(format!("invalid `{name}` oauth2 flow: {e}")))
619    }
620
621    let raw = PropMap::<String, serde_json::Value>::deserialize(deserializer)?;
622    let mut flows = PropMap::new();
623    for (key, value) in raw {
624        let parsed = match &*key {
625            "implicit" => Flow::Implicit(flow(&key, value)?),
626            "password" => Flow::Password(flow(&key, value)?),
627            "clientCredentials" => Flow::ClientCredentials(flow(&key, value)?),
628            "authorizationCode" => Flow::AuthorizationCode(flow(&key, value)?),
629            "deviceAuthorization" => Flow::DeviceAuthorization(flow(&key, value)?),
630            // Unknown flow name: fall back to matching on shape.
631            _ => flow(&key, value)?,
632        };
633        flows.insert(key, parsed);
634    }
635    Ok(flows)
636}
637
638/// [`OAuth2`] flow configuration object.
639///
640///
641/// See more details at <https://spec.openapis.org/oas/latest.html#oauth-flows-object>.
642#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
643#[serde(untagged)]
644pub enum Flow {
645    /// Define device authorization [`Flow`] type. See [`DeviceAuthorization::new`] for usage
646    /// details. Added in OpenAPI 3.2.
647    ///
648    /// Declared first because this enum is untagged: the other variants ignore the unknown
649    /// `deviceAuthorizationUrl` key, so a device flow reached through a bare `Flow`
650    /// deserialization would otherwise be misparsed as [`Flow::Password`]. Inside an
651    /// [`OAuth2`] document the flow name decides the variant — see [`deserialize_flows`].
652    DeviceAuthorization(DeviceAuthorization),
653    /// Define implicit [`Flow`] type. See [`Implicit::new`] for usage details.
654    ///
655    /// Soon to be deprecated by <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics>.
656    Implicit(Implicit),
657    /// Define password [`Flow`] type. See [`Password::new`] for usage details.
658    Password(Password),
659    /// Define client credentials [`Flow`] type. See [`ClientCredentials::new`] for usage details.
660    ClientCredentials(ClientCredentials),
661    /// Define authorization code [`Flow`] type. See [`AuthorizationCode::new`] for usage details.
662    AuthorizationCode(AuthorizationCode),
663}
664
665impl Flow {
666    fn get_type_as_str(&self) -> &str {
667        match self {
668            Self::DeviceAuthorization(_) => "deviceAuthorization",
669            Self::Implicit(_) => "implicit",
670            Self::Password(_) => "password",
671            Self::ClientCredentials(_) => "clientCredentials",
672            Self::AuthorizationCode(_) => "authorizationCode",
673        }
674    }
675}
676
677/// Implicit [`Flow`] configuration for [`OAuth2`].
678#[non_exhaustive]
679#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
680#[serde(rename_all = "camelCase")]
681pub struct Implicit {
682    /// Authorization token url for the flow.
683    pub authorization_url: String,
684
685    /// Optional refresh token url for the flow.
686    #[serde(skip_serializing_if = "Option::is_none")]
687    pub refresh_url: Option<String>,
688
689    /// Scopes required by the flow.
690    #[serde(flatten)]
691    pub scopes: Scopes,
692
693    /// Optional extensions "x-something"
694    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
695    pub extensions: PropMap<String, serde_json::Value>,
696}
697
698impl Implicit {
699    /// Construct a new implicit oauth2 flow.
700    ///
701    /// Accepts two arguments: one which is authorization url and second map of scopes. Scopes can
702    /// also be an empty map.
703    ///
704    /// # Examples
705    ///
706    /// Creates a new implicit flow with scopes.
707    /// ```
708    /// # use salvo_oapi::security::{Implicit, Scopes};
709    /// Implicit::new(
710    ///     "https://localhost/auth/dialog",
711    ///     Scopes::from_iter([
712    ///         ("edit:items", "edit my items"),
713    ///         ("read:items", "read my items"),
714    ///     ]),
715    /// );
716    /// ```
717    ///
718    /// Creates a new implicit flow without any scopes.
719    /// ```
720    /// # use salvo_oapi::security::{Implicit, Scopes};
721    /// Implicit::new("https://localhost/auth/dialog", Scopes::new());
722    /// ```
723    pub fn new<S: Into<String>>(authorization_url: S, scopes: Scopes) -> Self {
724        Self {
725            authorization_url: authorization_url.into(),
726            refresh_url: None,
727            scopes,
728            extensions: Default::default(),
729        }
730    }
731
732    /// Construct a new implicit oauth2 flow with refresh url for getting refresh tokens.
733    ///
734    /// This is essentially same as [`Implicit::new`] but allows defining `refresh_url` for the
735    /// [`Implicit`] oauth2 flow.
736    ///
737    /// # Examples
738    ///
739    /// Create a new implicit OAuth2 flow with refresh token.
740    /// ```
741    /// # use salvo_oapi::security::{Implicit, Scopes};
742    /// Implicit::with_refresh_url(
743    ///     "https://localhost/auth/dialog",
744    ///     Scopes::new(),
745    ///     "https://localhost/refresh-token",
746    /// );
747    /// ```
748    pub fn with_refresh_url<S: Into<String>>(
749        authorization_url: S,
750        scopes: Scopes,
751        refresh_url: S,
752    ) -> Self {
753        Self {
754            authorization_url: authorization_url.into(),
755            refresh_url: Some(refresh_url.into()),
756            scopes,
757            extensions: Default::default(),
758        }
759    }
760
761    /// Add openapi extensions (`x-something`) for [`Implicit`].
762    #[must_use]
763    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
764        self.extensions = extensions;
765        self
766    }
767}
768
769/// Authorization code [`Flow`] configuration for [`OAuth2`].
770#[non_exhaustive]
771#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
772#[serde(rename_all = "camelCase")]
773pub struct AuthorizationCode {
774    /// Url for authorization token.
775    pub authorization_url: String,
776    /// Token url for the flow.
777    pub token_url: String,
778
779    /// Optional refresh token url for the flow.
780    #[serde(skip_serializing_if = "Option::is_none")]
781    pub refresh_url: Option<String>,
782
783    /// Scopes required by the flow.
784    #[serde(flatten)]
785    pub scopes: Scopes,
786
787    /// Optional extensions "x-something"
788    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
789    pub extensions: PropMap<String, serde_json::Value>,
790}
791
792impl AuthorizationCode {
793    /// Construct a new authorization code OAuth flow.
794    ///
795    /// Accepts three arguments: one which is authorization url, two a token url and
796    /// three, a map of scopes for the OAuth flow.
797    ///
798    /// # Examples
799    ///
800    /// Creates a new authorization code flow with scopes.
801    /// ```
802    /// # use salvo_oapi::security::{AuthorizationCode, Scopes};
803    /// AuthorizationCode::new(
804    ///     "https://localhost/auth/dialog",
805    ///     "https://localhost/token",
806    ///     Scopes::from_iter([
807    ///         ("edit:items", "edit my items"),
808    ///         ("read:items", "read my items"),
809    ///     ]),
810    /// );
811    /// ```
812    ///
813    /// Creates a new authorization code flow without any scopes.
814    /// ```
815    /// # use salvo_oapi::security::{AuthorizationCode, Scopes};
816    /// AuthorizationCode::new(
817    ///     "https://localhost/auth/dialog",
818    ///     "https://localhost/token",
819    ///     Scopes::new(),
820    /// );
821    /// ```
822    pub fn new<A: Into<String>, T: Into<String>>(
823        authorization_url: A,
824        token_url: T,
825        scopes: Scopes,
826    ) -> Self {
827        Self {
828            authorization_url: authorization_url.into(),
829            token_url: token_url.into(),
830            refresh_url: None,
831            scopes,
832            extensions: Default::default(),
833        }
834    }
835
836    /// Construct a new  [`AuthorizationCode`] OAuth2 flow with additional refresh token url.
837    ///
838    /// This is essentially same as [`AuthorizationCode::new`] but allows defining extra parameter
839    /// `refresh_url` for fetching refresh token.
840    ///
841    /// # Examples
842    ///
843    /// Create [`AuthorizationCode`] OAuth2 flow with refresh url.
844    /// ```
845    /// # use salvo_oapi::security::{AuthorizationCode, Scopes};
846    /// AuthorizationCode::with_refresh_url(
847    ///     "https://localhost/auth/dialog",
848    ///     "https://localhost/token",
849    ///     Scopes::new(),
850    ///     "https://localhost/refresh-token",
851    /// );
852    /// ```
853    pub fn with_refresh_url<S: Into<String>>(
854        authorization_url: S,
855        token_url: S,
856        scopes: Scopes,
857        refresh_url: S,
858    ) -> Self {
859        Self {
860            authorization_url: authorization_url.into(),
861            token_url: token_url.into(),
862            refresh_url: Some(refresh_url.into()),
863            scopes,
864            extensions: Default::default(),
865        }
866    }
867
868    /// Add openapi extensions (`x-something`) for [`AuthorizationCode`].
869    #[must_use]
870    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
871        self.extensions = extensions;
872        self
873    }
874}
875
876/// Password [`Flow`] configuration for [`OAuth2`].
877#[non_exhaustive]
878#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
879#[serde(rename_all = "camelCase")]
880pub struct Password {
881    /// Token url for this OAuth2 flow. OAuth2 standard requires TLS.
882    pub token_url: String,
883
884    /// Optional refresh token url.
885    #[serde(skip_serializing_if = "Option::is_none")]
886    pub refresh_url: Option<String>,
887
888    /// Scopes required by the flow.
889    #[serde(flatten)]
890    pub scopes: Scopes,
891
892    /// Optional extensions "x-something"
893    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
894    pub extensions: PropMap<String, serde_json::Value>,
895}
896
897impl Password {
898    /// Construct a new password OAuth flow.
899    ///
900    /// Accepts two arguments: one which is a token url and
901    /// two, a map of scopes for the OAuth flow.
902    ///
903    /// # Examples
904    ///
905    /// Creates a new password flow with scopes.
906    /// ```
907    /// # use salvo_oapi::security::{Password, Scopes};
908    /// Password::new(
909    ///     "https://localhost/token",
910    ///     Scopes::from_iter([
911    ///         ("edit:items", "edit my items"),
912    ///         ("read:items", "read my items"),
913    ///     ]),
914    /// );
915    /// ```
916    ///
917    /// Creates a new password flow without any scopes.
918    /// ```
919    /// # use salvo_oapi::security::{Password, Scopes};
920    /// Password::new("https://localhost/token", Scopes::new());
921    /// ```
922    pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
923        Self {
924            token_url: token_url.into(),
925            refresh_url: None,
926            scopes,
927            extensions: Default::default(),
928        }
929    }
930
931    /// Construct a new password OAuth flow with an additional refresh URL.
932    ///
933    /// This is essentially same as [`Password::new`] but allows defining third parameter for
934    /// `refresh_url` for fetching refresh tokens.
935    ///
936    /// # Examples
937    ///
938    /// Creates a new password flow with a refresh URL.
939    /// ```
940    /// # use salvo_oapi::security::{Password, Scopes};
941    /// Password::with_refresh_url(
942    ///     "https://localhost/token",
943    ///     Scopes::from_iter([
944    ///         ("edit:items", "edit my items"),
945    ///         ("read:items", "read my items"),
946    ///     ]),
947    ///     "https://localhost/refres-token",
948    /// );
949    /// ```
950    pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
951        Self {
952            token_url: token_url.into(),
953            refresh_url: Some(refresh_url.into()),
954            scopes,
955            extensions: Default::default(),
956        }
957    }
958
959    /// Add openapi extensions (`x-something`) for [`Password`].
960    #[must_use]
961    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
962        self.extensions = extensions;
963        self
964    }
965}
966
967/// Client credentials [`Flow`] configuration for [`OAuth2`].
968#[non_exhaustive]
969#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
970#[serde(rename_all = "camelCase")]
971pub struct ClientCredentials {
972    /// Token url used for [`ClientCredentials`] flow. OAuth2 standard requires TLS.
973    pub token_url: String,
974
975    /// Optional refresh token url.
976    #[serde(skip_serializing_if = "Option::is_none")]
977    pub refresh_url: Option<String>,
978
979    /// Scopes required by the flow.
980    #[serde(flatten)]
981    pub scopes: Scopes,
982
983    /// Optional extensions "x-something"
984    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
985    pub extensions: PropMap<String, serde_json::Value>,
986}
987
988impl ClientCredentials {
989    /// Construct a new client credentials OAuth flow.
990    ///
991    /// Accepts two arguments: one which is a token url and
992    /// two, a map of scopes for the OAuth flow.
993    ///
994    /// # Examples
995    ///
996    /// Creates a new client credentials flow with scopes.
997    /// ```
998    /// # use salvo_oapi::security::{ClientCredentials, Scopes};
999    /// ClientCredentials::new(
1000    ///     "https://localhost/token",
1001    ///     Scopes::from_iter([
1002    ///         ("edit:items", "edit my items"),
1003    ///         ("read:items", "read my items"),
1004    ///     ]),
1005    /// );
1006    /// ```
1007    ///
1008    /// Creates a new client credentials flow without any scopes.
1009    /// ```
1010    /// # use salvo_oapi::security::{ClientCredentials, Scopes};
1011    /// ClientCredentials::new("https://localhost/token", Scopes::new());
1012    /// ```
1013    pub fn new<S: Into<String>>(token_url: S, scopes: Scopes) -> Self {
1014        Self {
1015            token_url: token_url.into(),
1016            refresh_url: None,
1017            scopes,
1018            extensions: Default::default(),
1019        }
1020    }
1021
1022    /// Construct a new client credentials OAuth flow with an additional refresh URL.
1023    ///
1024    /// This is essentially same as [`ClientCredentials::new`] but allows defining third parameter
1025    /// for `refresh_url`.
1026    ///
1027    /// # Examples
1028    ///
1029    /// Creates a new client credentials flow with a refresh URL.
1030    /// ```
1031    /// # use salvo_oapi::security::{ClientCredentials, Scopes};
1032    /// ClientCredentials::with_refresh_url(
1033    ///     "https://localhost/token",
1034    ///     Scopes::from_iter([
1035    ///         ("edit:items", "edit my items"),
1036    ///         ("read:items", "read my items"),
1037    ///     ]),
1038    ///     "https://localhost/refresh-url",
1039    /// );
1040    /// ```
1041    pub fn with_refresh_url<S: Into<String>>(token_url: S, scopes: Scopes, refresh_url: S) -> Self {
1042        Self {
1043            token_url: token_url.into(),
1044            refresh_url: Some(refresh_url.into()),
1045            scopes,
1046            extensions: Default::default(),
1047        }
1048    }
1049
1050    /// Add openapi extensions (`x-something`) for [`ClientCredentials`].
1051    #[must_use]
1052    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
1053        self.extensions = extensions;
1054        self
1055    }
1056}
1057
1058/// Device authorization [`Flow`] configuration for [`OAuth2`], as defined by
1059/// [RFC8628](https://tools.ietf.org/html/rfc8628). Added in OpenAPI 3.2.
1060#[non_exhaustive]
1061#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
1062#[serde(rename_all = "camelCase")]
1063pub struct DeviceAuthorization {
1064    /// Device authorization url for this flow. OAuth2 standard requires TLS.
1065    pub device_authorization_url: String,
1066
1067    /// Token url for this flow. OAuth2 standard requires TLS.
1068    pub token_url: String,
1069
1070    /// Optional refresh token url.
1071    #[serde(skip_serializing_if = "Option::is_none")]
1072    pub refresh_url: Option<String>,
1073
1074    /// Scopes required by the flow.
1075    #[serde(flatten)]
1076    pub scopes: Scopes,
1077
1078    /// Optional extensions "x-something"
1079    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
1080    pub extensions: PropMap<String, serde_json::Value>,
1081}
1082
1083impl DeviceAuthorization {
1084    /// Construct a new device authorization OAuth flow.
1085    ///
1086    /// # Examples
1087    ///
1088    /// ```
1089    /// # use salvo_oapi::security::{DeviceAuthorization, Scopes};
1090    /// DeviceAuthorization::new(
1091    ///     "https://localhost/device_authorization",
1092    ///     "https://localhost/token",
1093    ///     Scopes::from_iter([("edit:items", "edit my items")]),
1094    /// );
1095    /// ```
1096    pub fn new<S: Into<String>>(device_authorization_url: S, token_url: S, scopes: Scopes) -> Self {
1097        Self {
1098            device_authorization_url: device_authorization_url.into(),
1099            token_url: token_url.into(),
1100            refresh_url: None,
1101            scopes,
1102            extensions: Default::default(),
1103        }
1104    }
1105
1106    /// Construct a new device authorization OAuth flow with an additional refresh URL.
1107    ///
1108    /// # Examples
1109    ///
1110    /// ```
1111    /// # use salvo_oapi::security::{DeviceAuthorization, Scopes};
1112    /// DeviceAuthorization::with_refresh_url(
1113    ///     "https://localhost/device_authorization",
1114    ///     "https://localhost/token",
1115    ///     Scopes::new(),
1116    ///     "https://localhost/refresh-token",
1117    /// );
1118    /// ```
1119    pub fn with_refresh_url<S: Into<String>>(
1120        device_authorization_url: S,
1121        token_url: S,
1122        scopes: Scopes,
1123        refresh_url: S,
1124    ) -> Self {
1125        Self {
1126            device_authorization_url: device_authorization_url.into(),
1127            token_url: token_url.into(),
1128            refresh_url: Some(refresh_url.into()),
1129            scopes,
1130            extensions: Default::default(),
1131        }
1132    }
1133
1134    /// Add openapi extensions (`x-something`) for [`DeviceAuthorization`].
1135    #[must_use]
1136    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
1137        self.extensions = extensions;
1138        self
1139    }
1140}
1141
1142/// [`OAuth2`] flow scopes object defines required permissions for an OAuth flow.
1143///
1144/// Scopes must be given to oauth2 flow but depending on need one of few initialization methods
1145/// could be used.
1146///
1147/// * Create empty map of scopes you can use [`Scopes::new`].
1148/// * Create map with only one scope you can use [`Scopes::one`].
1149/// * Create multiple scopes from iterator with [`Scopes::from_iter`].
1150///
1151/// # Examples
1152///
1153/// Create empty map of scopes.
1154/// ```
1155/// # use salvo_oapi::security::Scopes;
1156/// let scopes = Scopes::new();
1157/// ```
1158///
1159/// Create [`Scopes`] holding one scope.
1160/// ```
1161/// # use salvo_oapi::security::Scopes;
1162/// let scopes = Scopes::one("edit:item", "edit pets");
1163/// ```
1164///
1165/// Create map of scopes from iterator.
1166/// ```
1167/// # use salvo_oapi::security::Scopes;
1168/// let scopes = Scopes::from_iter([
1169///     ("edit:items", "edit my items"),
1170///     ("read:items", "read my items"),
1171/// ]);
1172/// ```
1173#[derive(Default, Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
1174pub struct Scopes {
1175    scopes: PropMap<String, String>,
1176}
1177
1178impl Scopes {
1179    /// Construct new [`Scopes`] with empty map of scopes. This is useful if an OAuth flow does not
1180    /// need any permission scopes.
1181    ///
1182    /// # Examples
1183    ///
1184    /// Create empty map of scopes.
1185    /// ```
1186    /// # use salvo_oapi::security::Scopes;
1187    /// let scopes = Scopes::new();
1188    /// ```
1189    #[must_use]
1190    pub fn new() -> Self {
1191        Default::default()
1192    }
1193
1194    /// Construct new [`Scopes`] with holding one scope.
1195    ///
1196    /// * `scope` Is be the permission required.
1197    /// * `description` Short description about the permission.
1198    ///
1199    /// # Examples
1200    ///
1201    /// Create map of scopes with one scope item.
1202    /// ```
1203    /// # use salvo_oapi::security::Scopes;
1204    /// let scopes = Scopes::one("edit:item", "edit items");
1205    /// ```
1206    #[must_use]
1207    pub fn one<S: Into<String>>(scope: S, description: S) -> Self {
1208        Self {
1209            scopes: PropMap::from_iter(iter::once_with(|| (scope.into(), description.into()))),
1210        }
1211    }
1212}
1213
1214impl<I> FromIterator<(I, I)> for Scopes
1215where
1216    I: Into<String>,
1217{
1218    fn from_iter<T: IntoIterator<Item = (I, I)>>(iter: T) -> Self {
1219        Self {
1220            scopes: iter
1221                .into_iter()
1222                .map(|(key, value)| (key.into(), value.into()))
1223                .collect(),
1224        }
1225    }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230    use super::*;
1231
1232    macro_rules! test_fn {
1233        ($name:ident : $schema:expr; $expected:literal) => {
1234            #[test]
1235            fn $name() {
1236                let value = serde_json::to_value($schema).unwrap();
1237                let expected_value: serde_json::Value = serde_json::from_str($expected).unwrap();
1238
1239                assert_eq!(
1240                    value,
1241                    expected_value,
1242                    "testing serializing \"{}\": \nactual:\n{}\nexpected:\n{}",
1243                    stringify!($name),
1244                    value,
1245                    expected_value
1246                );
1247
1248                println!("{}", &serde_json::to_string_pretty(&$schema).unwrap());
1249            }
1250        };
1251    }
1252
1253    test_fn! {
1254        security_scheme_correct_default_http_auth:
1255        SecurityScheme::Http(Http::new(HttpAuthScheme::default()));
1256        r###"{
1257  "type": "http",
1258  "scheme": "basic"
1259}"###
1260    }
1261
1262    test_fn! {
1263        security_scheme_correct_http_bearer_json:
1264        SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT"));
1265        r###"{
1266  "type": "http",
1267  "scheme": "bearer",
1268  "bearerFormat": "JWT"
1269}"###
1270    }
1271
1272    test_fn! {
1273        security_scheme_correct_basic_auth:
1274        SecurityScheme::Http(Http::new(HttpAuthScheme::Basic));
1275        r###"{
1276  "type": "http",
1277  "scheme": "basic"
1278}"###
1279    }
1280
1281    test_fn! {
1282        security_scheme_correct_basic_auth_change_to_digest_auth_with_description:
1283        SecurityScheme::Http(Http::new(HttpAuthScheme::Basic).scheme(HttpAuthScheme::Digest).description(String::from("digest auth")));
1284        r###"{
1285  "type": "http",
1286  "scheme": "digest",
1287  "description": "digest auth"
1288}"###
1289    }
1290
1291    test_fn! {
1292        security_scheme_correct_digest_auth:
1293        SecurityScheme::Http(Http::new(HttpAuthScheme::Digest));
1294        r###"{
1295  "type": "http",
1296  "scheme": "digest"
1297}"###
1298    }
1299
1300    test_fn! {
1301        security_scheme_correct_hoba_auth:
1302        SecurityScheme::Http(Http::new(HttpAuthScheme::Hoba));
1303        r###"{
1304  "type": "http",
1305  "scheme": "hoba"
1306}"###
1307    }
1308
1309    test_fn! {
1310        security_scheme_correct_mutual_auth:
1311        SecurityScheme::Http(Http::new(HttpAuthScheme::Mutual));
1312        r###"{
1313  "type": "http",
1314  "scheme": "mutual"
1315}"###
1316    }
1317
1318    test_fn! {
1319        security_scheme_correct_negotiate_auth:
1320        SecurityScheme::Http(Http::new(HttpAuthScheme::Negotiate));
1321        r###"{
1322  "type": "http",
1323  "scheme": "negotiate"
1324}"###
1325    }
1326
1327    test_fn! {
1328        security_scheme_correct_oauth_auth:
1329        SecurityScheme::Http(Http::new(HttpAuthScheme::OAuth));
1330        r###"{
1331  "type": "http",
1332  "scheme": "oauth"
1333}"###
1334    }
1335
1336    test_fn! {
1337        security_scheme_correct_scram_sha1_auth:
1338        SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha1));
1339        r###"{
1340  "type": "http",
1341  "scheme": "scram-sha-1"
1342}"###
1343    }
1344
1345    test_fn! {
1346        security_scheme_correct_scram_sha256_auth:
1347        SecurityScheme::Http(Http::new(HttpAuthScheme::ScramSha256));
1348        r###"{
1349  "type": "http",
1350  "scheme": "scram-sha-256"
1351}"###
1352    }
1353
1354    test_fn! {
1355        security_scheme_correct_api_key_cookie_auth:
1356        SecurityScheme::from(ApiKey::Cookie(ApiKeyValue::new(String::from("api_key"))));
1357        r###"{
1358  "type": "apiKey",
1359  "name": "api_key",
1360  "in": "cookie"
1361}"###
1362    }
1363
1364    test_fn! {
1365        security_scheme_correct_api_key_header_auth:
1366        SecurityScheme::from(ApiKey::Header(ApiKeyValue::new("api_key")));
1367        r###"{
1368  "type": "apiKey",
1369  "name": "api_key",
1370  "in": "header"
1371}"###
1372    }
1373
1374    test_fn! {
1375        security_scheme_correct_api_key_query_auth:
1376        SecurityScheme::from(ApiKey::Query(ApiKeyValue::new(String::from("api_key"))));
1377        r###"{
1378  "type": "apiKey",
1379  "name": "api_key",
1380  "in": "query"
1381}"###
1382    }
1383
1384    test_fn! {
1385        security_scheme_correct_api_key_query_auth_with_description:
1386        SecurityScheme::from(ApiKey::Query(ApiKeyValue::with_description(String::from("api_key"), String::from("my api_key"))));
1387        r###"{
1388  "type": "apiKey",
1389  "name": "api_key",
1390  "description": "my api_key",
1391  "in": "query"
1392}"###
1393    }
1394
1395    test_fn! {
1396        security_scheme_correct_open_id_connect_auth:
1397        SecurityScheme::from(OpenIdConnect::new("https://localhost/openid"));
1398        r###"{
1399  "type": "openIdConnect",
1400  "openIdConnectUrl": "https://localhost/openid"
1401}"###
1402    }
1403
1404    test_fn! {
1405        security_scheme_correct_open_id_connect_auth_with_description:
1406        SecurityScheme::from(OpenIdConnect::with_description("https://localhost/openid", "OpenIdConnect auth"));
1407        r###"{
1408  "type": "openIdConnect",
1409  "openIdConnectUrl": "https://localhost/openid",
1410  "description": "OpenIdConnect auth"
1411}"###
1412    }
1413
1414    test_fn! {
1415        security_scheme_correct_oauth2_implicit:
1416        SecurityScheme::from(
1417            OAuth2::with_description([Flow::Implicit(
1418                Implicit::new(
1419                    "https://localhost/auth/dialog",
1420                    Scopes::from_iter([
1421                        ("edit:items", "edit my items"),
1422                        ("read:items", "read my items")
1423                    ]),
1424                ),
1425            )], "my oauth2 flow")
1426        );
1427        r###"{
1428  "type": "oauth2",
1429  "flows": {
1430    "implicit": {
1431      "authorizationUrl": "https://localhost/auth/dialog",
1432      "scopes": {
1433        "edit:items": "edit my items",
1434        "read:items": "read my items"
1435      }
1436    }
1437  },
1438  "description": "my oauth2 flow"
1439}"###
1440    }
1441
1442    test_fn! {
1443        security_scheme_correct_oauth2_implicit_with_refresh_url:
1444        SecurityScheme::from(
1445            OAuth2::with_description([Flow::Implicit(
1446                Implicit::with_refresh_url(
1447                    "https://localhost/auth/dialog",
1448                    Scopes::from_iter([
1449                        ("edit:items", "edit my items"),
1450                        ("read:items", "read my items")
1451                    ]),
1452                    "https://localhost/refresh-token"
1453                ),
1454            )], "my oauth2 flow")
1455        );
1456        r###"{
1457  "type": "oauth2",
1458  "flows": {
1459    "implicit": {
1460      "authorizationUrl": "https://localhost/auth/dialog",
1461      "refreshUrl": "https://localhost/refresh-token",
1462      "scopes": {
1463        "edit:items": "edit my items",
1464        "read:items": "read my items"
1465      }
1466    }
1467  },
1468  "description": "my oauth2 flow"
1469}"###
1470    }
1471
1472    test_fn! {
1473        security_scheme_correct_oauth2_password:
1474        SecurityScheme::OAuth2(
1475            OAuth2::with_description([Flow::Password(
1476                Password::new(
1477                    "https://localhost/oauth/token",
1478                    Scopes::from_iter([
1479                        ("edit:items", "edit my items"),
1480                        ("read:items", "read my items")
1481                    ])
1482                ),
1483            )], "my oauth2 flow")
1484        );
1485        r###"{
1486  "type": "oauth2",
1487  "flows": {
1488    "password": {
1489      "tokenUrl": "https://localhost/oauth/token",
1490      "scopes": {
1491        "edit:items": "edit my items",
1492        "read:items": "read my items"
1493      }
1494    }
1495  },
1496  "description": "my oauth2 flow"
1497}"###
1498    }
1499
1500    test_fn! {
1501        security_scheme_correct_oauth2_password_with_refresh_url:
1502        SecurityScheme::OAuth2(
1503            OAuth2::with_description([Flow::Password(
1504                Password::with_refresh_url(
1505                    "https://localhost/oauth/token",
1506                    Scopes::from_iter([
1507                        ("edit:items", "edit my items"),
1508                        ("read:items", "read my items")
1509                    ]),
1510                    "https://localhost/refresh/token"
1511                ),
1512            )], "my oauth2 flow")
1513        );
1514        r###"{
1515  "type": "oauth2",
1516  "flows": {
1517    "password": {
1518      "tokenUrl": "https://localhost/oauth/token",
1519      "refreshUrl": "https://localhost/refresh/token",
1520      "scopes": {
1521        "edit:items": "edit my items",
1522        "read:items": "read my items"
1523      }
1524    }
1525  },
1526  "description": "my oauth2 flow"
1527}"###
1528    }
1529
1530    test_fn! {
1531        security_scheme_correct_oauth2_client_credentials:
1532        SecurityScheme::OAuth2(
1533            OAuth2::new([Flow::ClientCredentials(
1534                ClientCredentials::new(
1535                    "https://localhost/oauth/token",
1536                    Scopes::from_iter([
1537                        ("edit:items", "edit my items"),
1538                        ("read:items", "read my items")
1539                    ])
1540                ),
1541            )])
1542        );
1543        r###"{
1544  "type": "oauth2",
1545  "flows": {
1546    "clientCredentials": {
1547      "tokenUrl": "https://localhost/oauth/token",
1548      "scopes": {
1549        "edit:items": "edit my items",
1550        "read:items": "read my items"
1551      }
1552    }
1553  }
1554}"###
1555    }
1556
1557    test_fn! {
1558        security_scheme_correct_oauth2_client_credentials_with_refresh_url:
1559        SecurityScheme::OAuth2(
1560            OAuth2::new([Flow::ClientCredentials(
1561                ClientCredentials::with_refresh_url(
1562                    "https://localhost/oauth/token",
1563                    Scopes::from_iter([
1564                        ("edit:items", "edit my items"),
1565                        ("read:items", "read my items")
1566                    ]),
1567                    "https://localhost/refresh/token"
1568                ),
1569            )])
1570        );
1571        r###"{
1572  "type": "oauth2",
1573  "flows": {
1574    "clientCredentials": {
1575      "tokenUrl": "https://localhost/oauth/token",
1576      "refreshUrl": "https://localhost/refresh/token",
1577      "scopes": {
1578        "edit:items": "edit my items",
1579        "read:items": "read my items"
1580      }
1581    }
1582  }
1583}"###
1584    }
1585
1586    test_fn! {
1587        security_scheme_correct_oauth2_authorization_code:
1588        SecurityScheme::OAuth2(
1589            OAuth2::new([Flow::AuthorizationCode(
1590                AuthorizationCode::with_refresh_url(
1591                    "https://localhost/authorization/token",
1592                    "https://localhost/token/url",
1593                    Scopes::from_iter([
1594                        ("edit:items", "edit my items"),
1595                        ("read:items", "read my items")
1596                    ]),
1597                    "https://localhost/refresh/token"
1598                ),
1599            )])
1600        );
1601        r###"{
1602  "type": "oauth2",
1603  "flows": {
1604    "authorizationCode": {
1605      "authorizationUrl": "https://localhost/authorization/token",
1606      "tokenUrl": "https://localhost/token/url",
1607      "refreshUrl": "https://localhost/refresh/token",
1608      "scopes": {
1609        "edit:items": "edit my items",
1610        "read:items": "read my items"
1611      }
1612    }
1613  }
1614}"###
1615    }
1616
1617    test_fn! {
1618        security_scheme_correct_oauth2_authorization_code_no_scopes:
1619        SecurityScheme::OAuth2(
1620            OAuth2::new([Flow::AuthorizationCode(
1621                AuthorizationCode::new(
1622                    "https://localhost/authorization/token",
1623                    "https://localhost/token/url",
1624                    Scopes::new()
1625                ),
1626            )])
1627        );
1628        r###"{
1629  "type": "oauth2",
1630  "flows": {
1631    "authorizationCode": {
1632      "authorizationUrl": "https://localhost/authorization/token",
1633      "tokenUrl": "https://localhost/token/url",
1634      "scopes": {}
1635    }
1636  }
1637}"###
1638    }
1639
1640    test_fn! {
1641        security_scheme_correct_oauth2_authorization_code_one_scopes:
1642        SecurityScheme::OAuth2(
1643            OAuth2::new([Flow::AuthorizationCode(
1644                AuthorizationCode::new(
1645                    "https://localhost/authorization/token",
1646                    "https://localhost/token/url",
1647                    Scopes::one("edit:items", "edit my items")
1648                ),
1649            )])
1650        );
1651        r###"{
1652  "type": "oauth2",
1653  "flows": {
1654    "authorizationCode": {
1655      "authorizationUrl": "https://localhost/authorization/token",
1656      "tokenUrl": "https://localhost/token/url",
1657      "scopes": {
1658        "edit:items": "edit my items"
1659      }
1660    }
1661  }
1662}"###
1663    }
1664
1665    test_fn! {
1666        security_scheme_correct_mutual_tls:
1667        SecurityScheme::MutualTls {
1668            description: Some(String::from("authorization is performed with client side certificate")),
1669            deprecated: None
1670        };
1671        r###"{
1672  "type": "mutualTLS",
1673  "description": "authorization is performed with client side certificate"
1674}"###
1675    }
1676
1677    #[test]
1678    fn security_requirement_accepts_uri_references() {
1679        // OpenAPI 3.2 allows a requirement name to be a URI, a `./`-prefixed relative
1680        // reference disambiguating a component-name collision, or a plain component name.
1681        let requirement = SecurityRequirement::new("api_key", Vec::<String>::new())
1682            .add("./foo", ["read:items"])
1683            .add("https://example.com/schemes.json#/oauth", ["write:items"]);
1684
1685        let value = serde_json::to_value(&requirement).expect("serialize");
1686        assert_eq!(
1687            value,
1688            serde_json::json!({
1689                "api_key": [],
1690                "./foo": ["read:items"],
1691                "https://example.com/schemes.json#/oauth": ["write:items"]
1692            })
1693        );
1694
1695        let parsed: SecurityRequirement = serde_json::from_value(value).expect("deserialize");
1696        assert_eq!(parsed, requirement);
1697    }
1698
1699    #[test]
1700    fn device_authorization_flow_round_trips_under_its_own_key() {
1701        let scheme = SecurityScheme::OAuth2(OAuth2::new([Flow::DeviceAuthorization(
1702            DeviceAuthorization::with_refresh_url(
1703                "https://localhost/device_authorization",
1704                "https://localhost/token",
1705                Scopes::one("edit:items", "edit my items"),
1706                "https://localhost/refresh",
1707            ),
1708        )]));
1709
1710        let value = serde_json::to_value(&scheme).expect("serialize");
1711        assert_eq!(
1712            value,
1713            serde_json::json!({
1714                "type": "oauth2",
1715                "flows": {
1716                    "deviceAuthorization": {
1717                        "deviceAuthorizationUrl": "https://localhost/device_authorization",
1718                        "tokenUrl": "https://localhost/token",
1719                        "refreshUrl": "https://localhost/refresh",
1720                        "scopes": { "edit:items": "edit my items" }
1721                    }
1722                }
1723            })
1724        );
1725
1726        // `Flow` is untagged, so the device flow must not be swallowed by `Password`, whose
1727        // fields are a subset of it.
1728        let parsed: SecurityScheme = serde_json::from_value(value).expect("deserialize");
1729        assert_eq!(parsed, scheme);
1730    }
1731
1732    #[test]
1733    fn oauth2_metadata_url_and_deprecated_serialize() {
1734        let scheme = SecurityScheme::OAuth2(
1735            OAuth2::new([Flow::ClientCredentials(ClientCredentials::new(
1736                "https://localhost/token",
1737                Scopes::new(),
1738            ))])
1739            .oauth2_metadata_url("https://localhost/.well-known/oauth-authorization-server")
1740            .deprecated(true),
1741        );
1742
1743        let value = serde_json::to_value(&scheme).expect("serialize");
1744        assert_eq!(
1745            value["oauth2MetadataUrl"],
1746            serde_json::json!("https://localhost/.well-known/oauth-authorization-server")
1747        );
1748        assert_eq!(value["deprecated"], serde_json::json!(true));
1749
1750        let parsed: SecurityScheme = serde_json::from_value(value).expect("deserialize");
1751        assert_eq!(parsed, scheme);
1752    }
1753
1754    #[test]
1755    fn deprecated_is_available_on_every_scheme_kind() {
1756        for scheme in [
1757            SecurityScheme::Http(Http::new(HttpAuthScheme::Basic).deprecated(true)),
1758            SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("api_key").deprecated(true))),
1759            SecurityScheme::OpenIdConnect(
1760                OpenIdConnect::new("https://localhost/openid").deprecated(true),
1761            ),
1762            SecurityScheme::MutualTls {
1763                description: None,
1764                deprecated: Some(true),
1765            },
1766        ] {
1767            let value = serde_json::to_value(&scheme).expect("serialize");
1768            assert_eq!(
1769                value["deprecated"],
1770                serde_json::json!(true),
1771                "deprecated missing from {value}"
1772            );
1773            let parsed: SecurityScheme = serde_json::from_value(value).expect("deserialize");
1774            assert_eq!(parsed, scheme);
1775        }
1776    }
1777}