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