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