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