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