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