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