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