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