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